模組四|剪輯管線(Day 17–22)
這是這一章最貴的一次除錯,也是我留下的紀錄裡寫得最完整的一筆。
那份剪輯記錄的小節標題直接寫著:
⚠️ 時間軸不同步(重要陷阱,日後同類錄音集數通用)
而同樣的診斷被複製進了剪輯腳本的檔頭註解,因為我知道自己會忘記。
線上多軌錄音會產生兩份東西:一份是經過降噪處理的音訊軌,一份是備份用的影音檔。
它們錄的是同一段對話,理論上應該完全同步。
實際上不是。
我拿降噪軌的某個聲音去對備份軌,量到的偏移是 +3.0 秒。
然後拿節目後段的另一個聲音去對,量到 +1.75 秒。
偏移量自己在變。

如果偏移是固定的 3 秒,事情很簡單:整條軌平移 3 秒就好。
而它從 3.0 漂到 1.75,這代表什麼?
兩台時鐘的速率不一樣。錄音過程中,一邊比另一邊跑得快一點點,於是差距隨時間累積或縮小。
就像兩支手錶,一支每小時快半秒:剛對時分毫不差,走一整天就差十幾秒。
那用線性映射呢?既然它是隨時間變的,找兩個點解出一條 a·t + b,不就好了?
也不行。記錄裡把它標成:
非線性漂移+跳點
「跳點」的意思是備份軌裡有多處小型插補:某些位置被塞了或吃掉了幾個 sample(取樣點——數位音訊會把聲音切成每秒幾萬個小格子,一格就是一個 sample)。
如果說速率差是一支手錶走得快一點,跳點就是有人趁你不注意把指針撥了一下。
一旦有跳點,就不存在一條連續的映射曲線。任何 a·t + b 在某些區段會很準、在另一些區段誤差達數百毫秒。
而數百毫秒,剛好就是一個字的長度。
這就是那句記錄的意思:
整體平移/線性 time-map 都會切到句中。
time-map(時間映射表)就是上面說的線性映射:一張把「備份軌的第幾秒」換算成「降噪軌的第幾秒」的對照公式。
而且還有第三層:段內還會再位移 ±0.3 到 0.45 秒。即使你把某一段的頭對準了,那一段結尾還是會偏。
第二版剪輯(v2)做了一個「雙錨點修正」:每一段取頭尾各 8 秒,各自對齊一次。
如果偏移是常數,頭尾算出來的 offset(偏移量——要把一條軌往前或往後推幾秒,才會跟另一條對上)應該一樣。
結果:26 段有段內位移。
這一步沒有修好任何東西,它的價值是證明了問題的形狀。在這之前我只知道「對不齊」,之後我知道「偏移量在段內都會變」。而這直接否決了所有全域解法。
先確定問題的形狀,再選演算法。這聽起來像廢話,而我是在猜了兩個版本之後才做這件事的。

最終的做法寫成虛擬碼是這樣——它不是可執行的程式,只是把「每一個剪點各自要做哪三件事」列出來:
對每一個剪點:
1. 從備份軌的該位置取 8–10 秒的音訊當錨
2. 在降噪軌的對應鄰域做 FFT 互相關
3. 取相關性最高的位移量 = 這個剪點專屬的 offset
每個剪點有自己的 offset。不求曲線、不求規律,就是解 N 個獨立的常數。
這個做法承認了一件事:這兩條軌之間的關係無法用一個簡潔的函數表達。與其硬套一個模型,不如在每個真正需要的位置各量一次。
互相關(cross-correlation)——把一段 8 秒的波形在另一條軌上滑動,算每個位置的相似度,最像的那個位置就是對齊點。前面那個 FFT(Fast Fourier Transform,快速傅立葉轉換)只是讓這種滑動比對算得快,不改變答案。
它為什麼有效:比對的是波形的形狀,跟內容無關。這不需要理解那段音訊是什麼,只需要兩條軌錄的是同一個聲音。
這是一個純訊號處理的解法,完全不涉及語意。對照 Day 18 那個「用音訊訊號回答有沒有聲音,用 ASR 回答說了什麼」,同一個分工原則。
附帶的一條規則寫在記錄裡:
剪點越少越安全。
因為每個剪點都是一次獨立的對齊,每一次都有失敗的機會。減少剪點就是減少風險。而這條規則後來影響了 Day 22 那個「從細剪改成大段保留」的決定。
這個坑不只出現一次。整理下來,同一族的問題有四個變體:
| 變體 | 現象 | 當時的處理 |
|---|---|---|
| 跨軌漂移(今天這個) | +3.0 → +1.75,非線性+跳點 | 每剪點 FFT 互相關 |
| 三軌長度不一致 | 兩軌差 1.50 秒(待確認軌數) | 常數平移三次:1.503991s → 累計 3.004s → 修到 3504ms |
| SRT 時間碼量化 | 被量化成整 2 秒,與音檔差約 3 秒 | 改用 word-level 時間戳 + 靜音偵測 |
| SRT 系統性早於語音 | 每句早約 0.7 秒 | 改用 RMS 包絡找停頓中點 |
表裡那幾個縮寫先解釋一下。SRT 是最常見的字幕檔格式,一行時間碼配一行字幕。word-level 時間戳是讓語音辨識輸出「每個字各自的開始與結束時間」,而不是整句共用一組時間。RMS 包絡(root mean square,均方根)則是把音量隨時間的起伏畫成一條線——用這條線找「這句講完了」的停頓,比看字幕的時間碼準得多。
第二個變體值得看:它就是「常數平移」的做法,而且平移了三次還在修(1.503991 秒 → 累計 3.004 秒 → 3504ms,即 3.504 秒)。
那正是今天這個診斷後來否決掉的做法。同一個問題,一集在猜,一集在解。而先發生的是猜的那次。
記錄裡還有一段方法論,我覺得比解法本身更有價值:
某支成品與其字幕零偏移。驗證方式:音量包絡顯示語音在 3215.00 秒戛然而止,與字幕最後一句的結束時間 3215.000 完全對齊,其後 32 秒為片尾音樂。
同一份音源,無漂移。
這是在主動證明一個否定命題:「這裡沒有問題」。
而它的方法很聰明:找一個訊號上有明確邊界的點(語音突然停止),量它,然後跟另一份資料的對應數字比。對得上,就證明了這兩份東西同源且未漂移。
從這裡可以提煉出一條通則:
偏移只發生在跨來源。不同的錄音軌、不同的轉檔管線、模型產生的時間碼,這三者之間會漂。而同一份音源,全程零偏移。
這條規則的實用價值很高:當你遇到對不齊的問題,先問「這兩份東西是不是同源」。同源還對不齊,先懷疑自己的計算,資料本身不會漂;不同源,那就別找一條曲線,去每個需要的位置各量一次。
這個解法慢。
每個剪點要跑一次 FFT 互相關,取 8 到 10 秒的窗口。剪點多的時候,光是對齊就要跑好一陣子。
而它的替代方案(常數平移)快得多,只是會切到句子中間。
第二個代價:它只解決了「這一種錄音來源」的問題。那份記錄的標題寫「日後同類錄音集數通用」,重點在「同類」兩個字。換一套錄音工具,漂移的形狀可能完全不同,前面的診斷全部要重來。
而我沒有把這個診斷流程寫成腳本。它現在是一段寫在記錄裡的敘述加上一支特定集數的程式碼。下次遇到,我要重讀那份記錄,然後手動複製那段程式碼。
第三個,最誠實的一個:這件事花掉的時間,遠遠超過那一集本身的價值。
如果當初直接接受「剪點少一點、每個剪點手動微調」,可能兩小時就結束了。而我花了兩個版本加一整套診斷。
我不後悔,因為那個診斷後來變成通用知識。但如果只做那一集,它不划算。
先量出問題的形狀,再選演算法。
我猜了兩個版本(常數平移、線性映射)才停下來做雙錨點量測。而那次量測(26 段有段內位移)直接否決了所有全域解法。如果一開始就做,會省掉兩個版本。
具體怎麼做:在兩個以上的位置各量一次同一個量。如果值一樣,它是常數;如果線性變化,用線性模型;如果既不一樣也不線性,別找曲線了,去每個需要的位置各量一次。
第二條:當關係無法用簡潔函數表達的時候,不要硬套模型。
解 N 個獨立常數看起來很笨,但它是對的。用一個錯的模型去逼近,會在你沒檢查的那些區段安靜地錯掉。而那些區段正好是你最不會去聽的地方。
第三條,很實用:
偏移只發生在跨來源。同一份音源、同一個管線出來的東西不會漂。所以遇到對不齊,第一件事是確認來源。如果同源還對不齊,那是你的計算錯了,不是資料漂了。
明天講剪輯的另一半:那些沒有客觀標準、只能靠試聽收斂的東西。